package com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema;

import java.util.ArrayList;
import java.util.List;

import com.db4o.odbgen.OdbgenException;

/**
 * 
 * Represents a type of a variable, field or method.
 * Examples: 'List<String>', 'int'.
 * See {@link TypeDeclaration} for more details.
 * @author liviug
 */
public class DataType {
	//IMPORTANT: Whenever adding/removing fields inside this class, be sure to update the copy() method.

	
	private String _package;
    private String _simpleName;
    private List<DataType> _typeArguments;

    
    /**
     * 
     * @author liviug
     * Helper class used to transform a type string into a tree.
     */
    private static class TypeTreeElement{
        private String name;
        private List<TypeTreeElement> children;

        public TypeTreeElement(String name, List<TypeTreeElement> children) throws OdbgenException {
            this.setName(name);
            this.setChildren(children);
        }
        public void setName(String name) throws OdbgenException {
            //Sanity checks.
            if(name == null){
                throw new OdbgenException("A name cannot be null inside a type tree.");
            }
            if(name.equals("")){
                throw new OdbgenException("A name cannot be empty inside a type tree.");
            }
            
            this.name = name;
        }
        public String getName() {
            return name;
        }
        public void setChildren(List<TypeTreeElement> children) throws OdbgenException {
            //Sanity checks.
            if(children == null){
                throw new OdbgenException("The children list cannot be null inside a type tree.");
            }

            this.children = children;
        }
        public List<TypeTreeElement> getChildren() {
            return children;
        }
    }
    
    /**
     * Constructs a DataType given it's components.
     * @param pkg The package.
     * @param simpleName The name.
     * @param typeArguments The list of generic types this type has.
     */
    public DataType(String pkg, String simpleName, List<DataType> typeArguments){
        this._package = pkg;
        this._simpleName = simpleName;
        this._typeArguments = typeArguments;
    }
    
    /**
     * Constructs a {@link DataType} given it's string representation.
     * @param dataTypeString The string representation.
     * @throws OdbgenException 
     */
    public DataType(String dataTypeString) throws OdbgenException {
        DataType dt = this.parseString(dataTypeString);
        this._package = dt._package;
        this._simpleName = dt._simpleName;
        this._typeArguments = dt._typeArguments;
    }

    /**
     * Returns the list of generic types this type has.
     */
    public List<DataType> getTypeArguments(){
        return this._typeArguments;
    }
    
    /**
     * Returns the name of this tipe.
     * This do not include any generics arguments.
     * Use getSimpleNameAndArguments() for this purpose.
     * @return
     */
    public String getSimpleName(){
        return this._simpleName;
    }

    /**
     * Returns the qualified name of this tipe.
     * This do not include any generics arguments.
     * Use getQualifiedNameAndArguments() for this purpose.
     * @return
     */
    public String getQualifiedName(){
        return this._package+(this._package.length()==0?"":".")+this._simpleName;
    }
    public String getPackage(){
        return this._package;
    }
    
    
    /**
     * Constructs a {@link DataType} given a string.
     * @throws OdbgenException 
     */
    private DataType parseString(String dataTypeString) throws OdbgenException {
        try{
            //Replace '(', ')' with '<', '>'. This hapens because in some cases paranthesis
            //are used in order to indicate generics arguments.
            dataTypeString = dataTypeString.replaceAll("\\(", "<");
            dataTypeString = dataTypeString.replaceAll("\\)", ">");

            //Eliminate all spaces. In order for the algorithm to work,
            //we have to do that.
            dataTypeString = dataTypeString.replaceAll("\\s", "");

            List<TypeTreeElement> typeTree = this.transformToTree(dataTypeString);

            //Sanity check: only one root element allowed.
            if(typeTree.size()!=1){
                throw new OdbgenException("The data type '%s' is bad formatted.", dataTypeString); 
            }

            TypeTreeElement root = typeTree.get(0);

            DataType dt = this.buildDataType(root);

            return dt;
        }
        catch(Exception e){
            throw new OdbgenException(e, "Syntax error while parsing type '%s'.", dataTypeString);
        }
    }

    private DataType buildDataType(TypeTreeElement root) {

        //Get simple name and package name.
        String qualifiedName = root.getName();
        String simpleName;
        String packageName;
        Utils.QualifiedName qname = Utils.getPackageAndSimpleName(qualifiedName);
        simpleName = qname.getSimpleName();
        packageName = qname.getPackageName();
        
        //Get the type arguments.
        List<DataType> typeArguments = new ArrayList<DataType>();
        for(TypeTreeElement child : root.getChildren()){
            DataType typeArgument = this.buildDataType(child);
            typeArguments.add(typeArgument);
        }
        
        //Construct the datatype.
        DataType res = new DataType(packageName, simpleName, typeArguments);
        
        
        return res;
    }

    
    /**
     * Receives a string like "a,b<x<y,c>,z>,d" and transforms it into a tree like this:
     * {"a","b"{"x"{"y","c"},"z"},"d"}
     * @param dataTypeString
     * @return
     * @throws OdbgenException 
     */
    private List<TypeTreeElement> transformToTree(String dataTypeString) throws OdbgenException {
        List<TypeTreeElement> res = new ArrayList<TypeTreeElement>();
        char[] chars = dataTypeString.toCharArray();
        StringBuilder currentName = new StringBuilder();
        boolean currentStringChanged = false;

        //Iterate each character
        for(int i = 0; i<chars.length; i++){
            if(this.isName(chars[i])){
                currentName.append(chars[i]);
                currentStringChanged = true;
            }
            else if(chars[i]==','){
                if(i==0){
                    //The string cannot start with a comma.
                    throw new OdbgenException("Type '%s' starts with a comma.", dataTypeString);
                }
                else if(chars[i-1]=='>'){
                    //We are at the end of an argument list.
                    //We do nothing here.
                }
                else if(this.isName(chars[i-1])){
                    //We are at the end of a name.
                    //The current name does not have any lists.
                    TypeTreeElement e = new TypeTreeElement(currentName.toString(), new ArrayList<TypeTreeElement>());
                    res.add(e);
                    currentName = new StringBuilder();
                    currentStringChanged = false;
                }
                else{
                    //Anything else is unacceptable.
                    throw new OdbgenException("Type '%s' contains invalid character before comma.", dataTypeString);
                }
            }
            else if(chars[i]=='<'){
                //We are at the begining of a new list.
                String content = this.getListContent(chars, i);
                TypeTreeElement e = new TypeTreeElement(
                        currentName.toString(), 
                        this.transformToTree(content)
                );
                res.add(e);
                i=i+content.length()+1;
                currentName = new StringBuilder();
                currentStringChanged = false;
            }
            else if(chars[i]=='>'){
                //This should never happen.
                throw new OdbgenException("Paranthesis mismatch inside '%s'.", dataTypeString);
            }
            else{
                throw new OdbgenException("Unknown character received in type: '%s'.", chars[i]);
            }

        }
        
        //If we still have a name in there, add it to the current list.
        if(currentStringChanged == true){
            TypeTreeElement e = new TypeTreeElement(currentName.toString(), new ArrayList<TypeTreeElement>());
            res.add(e);
        }
        
        
        return res;
    }

    /**
     * Receives a list of characters. At index i there must be an '<' character.
     * Returns the string contained between '<' and '>'. Between those two there may be
     * many imbricated '<' and '>'.
     * @throws OdbgenException 
     */
    private String getListContent(char[] chars, int offset) throws OdbgenException {
        
        StringBuilder res = new StringBuilder();
        int k = 1;
        //Iterate the string and build the result.
        for(int i = offset+1; i<chars.length; i++){
            char c = chars[i];
            if(c=='<')
                k++;
            if(c=='>')
                k--;
            if(k==0)
                break;
            res.append(c);
        }
        
        //The number of '<' and '>' must match.
        if(k!=0)
            throw new OdbgenException("Paranthesis mismatch inside '%s'.", String.valueOf(chars));

        
        return res.toString();
    }

    /**
     * Returns true if the given character is part of a type name.
     */
    private boolean isName(char c) {
        if(c=='.' || c=='_' || Character.isLetter(c) || Character.isDigit(c))
            return true;
        else
            return false;
    }

    /**
     * Returns the name and the arguments of this type.
     */
    public String getSimpleNameAndArguments() {
        StringBuilder res = new StringBuilder();
        res.append(this.getSimpleName());
        if(this._typeArguments.size()>0){
            res.append('<');
            int k=0;
            for(DataType dt : this._typeArguments){
                if(k!=0){
                    res.append(", ");
                }
                res.append(dt.toString());
                k++;
            }
            res.append('>');
        }
        return res.toString();
    }

    /**
     * Returns the qualified name and the arguments of this type.
     */
    public String getQualifiedNameAndArguments() {
        StringBuilder res = new StringBuilder();
        res.append(this.getSimpleName());
        if(this._typeArguments.size()>0){
            res.append('<');
            int k=0;
            for(DataType dt : this._typeArguments){
                if(k!=0){
                    res.append(", ");
                }
                res.append(dt.toString());
                k++;
            }
            res.append('>');
        }
        return res.toString();
    }
    
    /**
     * Returns true if this data type is a primitive type.
     * Otherwise returns false.
     */
    public boolean isPrimitiveType(){
        //Iterate all primitive types and see whether this type
        //corresponds.
        for(PrimitiveType val : PrimitiveType.values()){
            if(this.getSimpleName().equals(val.getName())){
                if(this.getPackage().isEmpty()){
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Returns the type argument used by SODA queries when dealing with generic collections.
     * The result is according to {@link CollectionType}.
     * Only generic collections with fully specified type argument list are recognized.
     * For example, for 'List', null is returned, while for 'List<String>', 'String' is returned.
     * Note that the matching is performed using just the simple name. Package names
     * are not considered.  
     */
    public DataType getGenericCollectionSodaArgument(){
    	for(CollectionType colType : CollectionType.values()){
    		if(this.getSimpleName().equals(colType.getSimpleName())){
    			//The collection must have the propper type arguments.
    			if(this.getTypeArguments().size() < colType.getSodaTypeArgumentOffset()+1){
    				return null;
    			}
    			else{
    				return this.getTypeArguments().get(colType.getSodaTypeArgumentOffset());
    			}
    		}
    	}
    	return null;
    }

	/**
	 * Duplicates this data type. 
	 * @return The duplicated data type.
	 */
	public DataType copy() {
		List<DataType> typeArguments = new ArrayList<DataType>();
		for(DataType typeArgument : this.getTypeArguments()){
			typeArguments.add(typeArgument.copy());
		}
		DataType res = new DataType(
				this.getPackage(), 
				this.getSimpleName(), 
				typeArguments
				);
		return res;
	}

    /**
     * Returns a string representation of this data type.
     */
    @Override
    public String toString() {
        return this.getQualifiedNameAndArguments();
    }

}
